package sxe.core.image;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import com.sun.image.codec.jpeg.*;

import java.awt.*;
import java.io.*;

import snobject.*;


/** 
 * ImageProcessing
 * @note Private constructor prevents instantiation of class
 * 		This class contains methods that operate on snImages and perform numerous Image Processing 
 * 		algorithms.
 */
public class ImageProcessing {

	/** 
	 * ImageProcessing constructor
	 * @note private to prevent instantiation of class
	 *
	 */
	private ImageProcessing() { }

	/** 
	 * Diff 
	 * @requires sni1 and sni2 need to have same dimensions
	 * @param snImage, snImage
	 * @return snImage containing Image Difference of two input snImages 
	 * 			(pixel-by-pixel subtraction of RGB values) 
	 */
	// TODO handle different snImage dimensions
	public static snImage Diff(snImage sni1, snImage sni2) {
		   /* do not directly manipulate the original image, instead work with a copy */
		   BufferedImage buffImg1 = sni1.getImageCopy(); 
		   BufferedImage buffImg2 = sni2.getImageCopy();
		   
		   Raster r1 = buffImg1.getRaster();
		   Raster r2 = buffImg2.getRaster();
		   
		   int argb1,argb2,red1,red2,green1,green2,blue1,blue2;
		   int newred,newgreen,newblue;
		   int rgb;
		   //int alpha1,alpha2;

		   // iterate down columns of pixels
		   for(int i=r1.getMinX();i<r1.getWidth();i++)
			   for(int j=r1.getMinY();j<r1.getHeight();j++){
					argb1 = buffImg1.getRGB(i,j);
					argb2 = buffImg2.getRGB(i,j);

					
					red1 = (argb1 >> 16) & 0xff;
					red2 = (argb2 >> 16) & 0xff;
					green1 = (argb1 >> 8) & 0xff;
					green2 = (argb2 >> 8) & 0xff;
					blue1 = argb1 & 0xff;
					blue2 = argb2 & 0xff;
					//int alpha1 = (argb1 >> 24) & 0xff;
					//int alpha2 = (argb2 >> 24) & 0xff;

					newred = Math.abs(red1 - red2);
					newgreen = Math.abs(green1 - green2);
					newblue = Math.abs(blue1 - blue2);
					
					rgb = (newred << 16) | (newgreen << 8) | (newblue);
					
					buffImg1.setRGB(i,j,rgb);
			   }
		   return new snImage(buffImg1);
	}

	/**
	 * Diff
	 * @param snImage, snImage, thresh:int
	 * @return int corresponding to number of changed pixels, determined by threshold parameter
	 * @requires sni1 and sni2 must have same dimensions
	 */
	//TODO Handle snImages of different dimensions
	public static int Diff(snImage sni1, snImage sni2, int thresh) {
		
		   /* do not directly manipulate the original image, instead work with a copy */
		   BufferedImage buffImg1 = sni1.getImageCopy(); 
		   BufferedImage buffImg2 = sni2.getImageCopy();
		   
		   Raster r1 = buffImg1.getRaster();
		   Raster r2 = buffImg2.getRaster();
		   
		   int argb1,argb2,red1,red2,green1,green2,blue1,blue2;
		   int newred,newgreen,newblue;

		   int changedPixelCount = 0;
		   
		   // iterate down columns of pixels
		   for(int i=r1.getMinX();i<r1.getWidth();i++)
			   for(int j=r1.getMinY();j<r1.getHeight();j++){
					argb1 = buffImg1.getRGB(i,j);
					argb2 = buffImg2.getRGB(i,j);

					
					red1 = (argb1 >> 16) & 0xff;
					red2 = (argb2 >> 16) & 0xff;
					green1 = (argb1 >> 8) & 0xff;
					green2 = (argb2 >> 8) & 0xff;
					blue1 = argb1 & 0xff;
					blue2 = argb2 & 0xff;
					//int alpha1 = (argb1 >> 24) & 0xff;
					//int alpha2 = (argb2 >> 24) & 0xff;

					newred = Math.abs(red1 - red2);
					newgreen = Math.abs(green1 - green2);
					newblue = Math.abs(blue1 - blue2);

					//black in Diff Image represents no change, white is largest change
					if (newred > thresh || newgreen > thresh || newblue > thresh) {
						changedPixelCount++;
					}					
			   }
		   return changedPixelCount;
		
	}

	/**
	 * RGBtoGrayscale
	 * @param snImage
	 * @return snImage with all pixels converted to grayscale value
	 */
	public static snImage RGBtoGrayscale(snImage sni) {
		   /* do not directly manipulate the original image, instead work with a copy */
		   BufferedImage buffImg1 = sni.getImageCopy(); 
		   
		   Raster r1 = buffImg1.getRaster();
		   
		   int argb,red,green,blue;

		   double newred,newgreen,newblue;
		   double intensity;
		   int grayscale;
		   
		   // iterate down columns of pixels
		   for(int i=r1.getMinX();i<r1.getWidth();i++)
			   for(int j=r1.getMinY();j<r1.getHeight();j++){
					argb = buffImg1.getRGB(i,j);
					
					//strip values from RGB-encoded int
					red = (argb >> 16) & 0xff;
					green = (argb >> 8) & 0xff;
					blue = argb & 0xff;

					//transform to intensity using function
					// I = .3R + .59G + .11B
					newred = .3 * (double)red;
					newgreen = .59 * (double)green;
					newblue = .11 * (double)blue;
					intensity = newred + newgreen + newblue;

					// grayscale has intensity as RGB values
					grayscale = ((int)(intensity) << 16)
						| ((int)(intensity) << 8)
						| ((int)(intensity));
					
					buffImg1.setRGB(i,j,grayscale);
			   }
		   return new snImage(buffImg1);
	}

	private static int DEFAULTPIXELRATIO = 2;
	
	/**
	 * DetectMotion
	 * @param snImage, snImage
	 * @return True if motion is detected between frames, False otherwise
	 * @requires sni1 and sni2 must have same dimensions, pixelRatio in range 0 to 100
	 * @effects If the number of pixels differing between the two frames exceeds
	 * 			ratio * totalPixels then return True, else False
	 */
	// TODO Handle snImages of different sizes
	public static boolean DetectMotion(snImage sni1, snImage sni2, int pixelRatio) {
		int Diffthresh = 25; 
		// Diffthresh on scale of 0 to 255, larger value allows for more robustness due to 
		// varying light intensities
		int changedPixels;
		int totalPixels;
		double ratio;
		
		if (pixelRatio >= 0 && pixelRatio <= 100) {
			ratio = (double) pixelRatio / 100.0; // percent of image's pixels			
		} else {
			System.err.println("pixelRatio in DetectMotion was out of range [0-100]. Defaulting to " + DEFAULTPIXELRATIO);
			ratio = DEFAULTPIXELRATIO;
		}

		totalPixels = sni1.getImage().getHeight() * sni1.getImage().getWidth();
		
		changedPixels = Diff(sni1,sni2,Diffthresh);
		System.out.println("Total Pixels: " + totalPixels);
		System.out.println("Ratio: " + ratio);
		System.out.println("Motion Threshold: " + ratio * totalPixels);
		System.out.println("Changed Pixels: " + changedPixels);
		if (changedPixels > ratio * totalPixels) {
			return true;
		} else {
			return false;
		}		
	}
	
	//uses DEFAULTPIXELRATIO	
	public static boolean DetectMotion(snImage sni1, snImage sni2) {
		return DetectMotion(sni1,sni2,DEFAULTPIXELRATIO);
	}

	/** 
	 * Thumbnail
	 * @param snImage
	 * @return snImage that is resized smaller, constrained by thumbnail dimension default values
	 */
	public static snImage Thumbnail(snImage sni, int width, int height) {
		BufferedImage buffImg = sni.getImageCopy();
		
	    int thumbWidth = width;
	    int thumbHeight = height;
	    double thumbRatio = (double)thumbWidth / (double)thumbHeight;
	    
	    int imageWidth = buffImg.getWidth(null);
	    int imageHeight = buffImg.getHeight(null);
	    double imageRatio = (double)imageWidth / (double)imageHeight;

	    //adapted from original author Marco Schmidt
	    // constrain thumbnail to be same ratio as original image
	    if (thumbRatio < imageRatio) {
	      thumbHeight = (int)(thumbWidth / imageRatio);
	    } else {
	      thumbWidth = (int)(thumbHeight * imageRatio);
	    }

	    // draw original image to thumbnail image object and
	    // scale it to the new size on-the-fly
	    BufferedImage thumbImage = new BufferedImage(thumbWidth, 
	      thumbHeight, BufferedImage.TYPE_INT_RGB);
	    Graphics2D graphics2D = thumbImage.createGraphics();
	    graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
	      RenderingHints.VALUE_INTERPOLATION_BILINEAR);
	    graphics2D.drawImage(buffImg, 0, 0, thumbWidth, thumbHeight, null);

		return new snImage(thumbImage);
		
	}
	

	// DEFAULT THUMBNAIL DIMENSIONS
	private static int DEFAULTTHUMBNAILWIDTH = 150;
	private static int DEFAULTTHUMBNAILHEIGHT = 150;

	/** 
	 * Thumbnail
	 * @param snImage
	 * @return snImage that is resized smaller, constrained by thumbnail dimension default values
	 */
	public static snImage Thumbnail(snImage sni) {
		return Thumbnail(sni,DEFAULTTHUMBNAILWIDTH,DEFAULTTHUMBNAILHEIGHT);
	}

	/** 
	 * loadSNImage
	 * @requires existence of filename in root folder of package
	 * @modifies 
	 * @effects loads image from file (png,jpg,gif supported) and returns as an snImage
	 * @throws InterruptedException if mediaTracker is interrupted reading from file
	 */
	// TODO Throw FileNotFoundException
	public static snImage loadSNImage(String filename) throws InterruptedException {

		try {
		Image image = Toolkit.getDefaultToolkit().getImage(filename);
	    MediaTracker mediaTracker = new MediaTracker(new Container());
	    mediaTracker.addImage(image, 0);
	    mediaTracker.waitForID(0);

	    //convert Image to BufferedImage for use with snImages
	    BufferedImage bufImage = new BufferedImage(image.getWidth(null),image.getHeight(null),BufferedImage.TYPE_INT_RGB);
	    Graphics2D graphics2D = bufImage.createGraphics();
	    graphics2D.drawImage(image, 0, 0, null);

	    return new snImage(bufImage);

		} catch (InterruptedException i) {
			throw i;
		}
	}
	
	
	/**
	 * saveSNImage
	 * @param snImage
	 * @param filename:String
	 * @param quality:int
	 * @return true if image successfully saved, IOException thrown otherwise
	 * @throws IOException
	 */
	public static boolean saveSNImage(snImage sni,String filename,int quality) throws IOException {
	   
		try {
		BufferedOutputStream out = new BufferedOutputStream(new
	    	      FileOutputStream(filename));
	    	    JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
	    	    JPEGEncodeParam param = encoder.
	    	      getDefaultJPEGEncodeParam(sni.getImage());
	    	    quality = Math.max(0, Math.min(quality, 100));
	    	    param.setQuality((float)quality / 100.0f, false);
	    	    encoder.setJPEGEncodeParam(param);
	    	    encoder.encode(sni.getImage());
	    	    out.close(); 
		} catch (IOException e) {
			throw new IOException("Failed to save snImage.");
		}
	    	    return true;
	}

	/**
	 * saveSNImage
	 * @note overloads with quality:int defaulting to 100
	 * @param snImage
	 * @param filename:String
	 * @return true if image successfully saved, IOException thrown otherwise
	 * @throws IOException
	 */	
	public static boolean saveSNImage(snImage sni, String filename) throws IOException {
		try {
			return saveSNImage(sni,filename,100);
		} catch (IOException i) {
			throw i;
		}
	}
	
}
